using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Configuration;
using System.Globalization;
using System.IO;
using System.Text;

namespace SCDL {
    // class generating code via CodeDom from ScdlDocument
    class ConfigCodeGenerator {
        CodeNamespace _generatedNamespace;

        public ConfigCodeGenerator() { 
        }

        public void GenerateCode(ScdlDocument doc, string outputLanguage, TextWriter outputCode) {
            // get the CodeDom provider for the language
            CodeDomProvider provider = CodeDomProvider.CreateProvider(outputLanguage);

            // generate the CodeDom tree
            CodeCompileUnit unit = GenerateCodeDomTree(doc);

            // generate source
            CodeGeneratorOptions options = new CodeGeneratorOptions();
            options.BlankLinesBetweenMembers = true;
            options.BracingStyle = "Block";
            options.ElseOnClosing = false;
            options.IndentString = "    ";

            provider.GenerateCodeFromCompileUnit(unit, outputCode, options);
        }

        public CodeCompileUnit GenerateCodeDomTree(ScdlDocument doc) {
            CodeCompileUnit unit = null;

            try {
                // generate namespace
                _generatedNamespace = new CodeNamespace(NameFromXmlName(doc.Namespace));
                _generatedNamespace.Imports.Add(new CodeNamespaceImport("System"));
                _generatedNamespace.Imports.Add(new CodeNamespaceImport("System.Configuration"));

                // for each node generate config section
                foreach (ScdlSection section in doc.Sections) {
                    AddConfigSection(section);
                }

                unit = new CodeCompileUnit(); 
                unit.Namespaces.Add(_generatedNamespace);
            }
            finally {
                _generatedNamespace = null;
            }

            return unit;
        }

        void AddConfigSection(ScdlSection section) {
            // create config section type
            string sectionTypeName = NameFromXmlName(section.Name) + "Section";
            CodeTypeDeclaration sectionType = new CodeTypeDeclaration(sectionTypeName);
            sectionType.BaseTypes.Add(typeof(ConfigurationSection));

            // fields from xml attributes
            foreach (ScdlAttribute attr in section.Attributes) {
                AddConfigAttribute(attr, sectionType);
            }

            // sub-elements
            foreach (ScdlNode element in section.SubNodes) {
                AddConfigElement(element, sectionType);
            }

            // sub-collections
            foreach (ScdlCollection collection in section.SubCollections) {
                AddConfigCollection(collection, sectionType);
            }

            // add the generated type to the namespace
            _generatedNamespace.Types.Add(sectionType);
        }

        void AddConfigCollection(ScdlCollection collection, CodeTypeDeclaration containingType) {
            
            // Generate element type

            string elementTypeName = NameFromXmlName(collection.Name) + "Element";
            CodeTypeDeclaration elementType = new CodeTypeDeclaration(elementTypeName);
            elementType.BaseTypes.Add(typeof(ConfigurationElement));

            CodeMemberProperty keyAttributeElementProperty = null;
            foreach (ScdlAttribute attr in collection.ItemAttributes) {
                if (attr.IsKey) {
                    // assume only one attribute is the key
                    AddConfigAttribute(attr, elementType, out keyAttributeElementProperty);
                }
                else {
                    AddConfigAttribute(attr, elementType);
                }
            }

            if (keyAttributeElementProperty == null) {
                // no attributes -> no collection
                return;
            }

            _generatedNamespace.Types.Add(elementType);

            // Generate collection type

            string collectionTypeName = NameFromXmlName(collection.Name) + "Collection";
            CodeTypeDeclaration collectionType = new CodeTypeDeclaration(collectionTypeName);
            collectionType.BaseTypes.Add(typeof(ConfigurationElementCollection));

            collectionType.CustomAttributes.Add(new CodeAttributeDeclaration(
                new CodeTypeReference(typeof(ConfigurationCollectionAttribute)),
                new CodeAttributeArgument[] {
                    new CodeAttributeArgument(new CodeTypeOfExpression(new CodeTypeReference(elementTypeName)))
                }));

            CodeMemberMethod m;

            m = new CodeMemberMethod();
            m.Name = "CreateNewElement";
            m.Attributes &= ~MemberAttributes.AccessMask;
            m.Attributes &= ~MemberAttributes.ScopeMask;
            m.Attributes |= MemberAttributes.Family | MemberAttributes.Override;
            m.ReturnType = new CodeTypeReference(typeof(ConfigurationElement));
            m.Statements.Add(new CodeMethodReturnStatement(new CodeObjectCreateExpression(elementTypeName)));
            collectionType.Members.Add(m);

            m = new CodeMemberMethod();
            m.Name = "GetElementKey";
            m.Attributes &= ~MemberAttributes.AccessMask;
            m.Attributes &= ~MemberAttributes.ScopeMask;
            m.Attributes |= MemberAttributes.Family | MemberAttributes.Override;
            m.ReturnType = new CodeTypeReference(typeof(object));
            m.Parameters.Add(new CodeParameterDeclarationExpression(typeof(ConfigurationElement), "element"));
            m.Statements.Add(new CodeMethodReturnStatement(
                new CodePropertyReferenceExpression(
                    new CodeCastExpression(elementTypeName, new CodeArgumentReferenceExpression("element")),
                    keyAttributeElementProperty.Name)));
            collectionType.Members.Add(m);

            m = new CodeMemberMethod();
            m.Name = "Add";
            m.Attributes &= ~MemberAttributes.AccessMask;
            m.Attributes |= MemberAttributes.Public;
            m.Parameters.Add(new CodeParameterDeclarationExpression(elementTypeName, "element"));
            m.Statements.Add(new CodeMethodInvokeExpression(new CodeThisReferenceExpression(), "BaseAdd",
                new CodeExpression[] { new CodeArgumentReferenceExpression("element") }));
            collectionType.Members.Add(m);

            m = new CodeMemberMethod();
            m.Name = "Remove";
            m.Attributes &= ~MemberAttributes.AccessMask;
            m.Attributes |= MemberAttributes.Public;
            m.Parameters.Add(new CodeParameterDeclarationExpression(keyAttributeElementProperty.Type, "key"));
            m.Statements.Add(new CodeMethodInvokeExpression(new CodeThisReferenceExpression(), "BaseRemove",
                new CodeExpression[] { new CodeArgumentReferenceExpression("key") }));
            collectionType.Members.Add(m);

            m = new CodeMemberMethod();
            m.Name = "Clear";
            m.Attributes &= ~MemberAttributes.AccessMask;
            m.Attributes |= MemberAttributes.Public;
            m.Statements.Add(new CodeMethodInvokeExpression(new CodeThisReferenceExpression(), "BaseClear"));
            collectionType.Members.Add(m);

            _generatedNamespace.Types.Add(collectionType);

            // Add collection property to the containing type

            string collectionTagName = collection.Name;
            string collectionPropertyName = NameFromXmlName(collectionTagName);

            CodeMemberProperty p = new CodeMemberProperty();
            p.Attributes &= ~MemberAttributes.AccessMask;
            p.Attributes |= MemberAttributes.Public;
            p.Name = collectionPropertyName;
            p.Type = new CodeTypeReference(collectionTypeName);

            p.CustomAttributes.Add(new CodeAttributeDeclaration(
                new CodeTypeReference(typeof(ConfigurationProperty)),
                new CodeAttributeArgument[] {
                    new CodeAttributeArgument(new CodePrimitiveExpression(collectionTagName))
                }));

            p.GetStatements.Add(new CodeMethodReturnStatement(
                new CodeCastExpression(collectionTypeName, new CodeIndexerExpression(
                new CodeBaseReferenceExpression(), new CodePrimitiveExpression(collectionTagName)))));

            containingType.Members.Add(p);
        }

        void AddConfigElement(ScdlNode element, CodeTypeDeclaration containingType) {
            // Generate element type

            string elementTypeName = NameFromXmlName(element.Name) + "Element";
            CodeTypeDeclaration elementType = new CodeTypeDeclaration(elementTypeName);
            elementType.BaseTypes.Add(typeof(ConfigurationElement));

            foreach (ScdlAttribute attr in element.Attributes) {
                AddConfigAttribute(attr, elementType);
            }

            _generatedNamespace.Types.Add(elementType);

            // Add element property to the containing type

            string elementTagName = element.Name;
            string elementPropertyName = NameFromXmlName(elementTagName);

            CodeMemberProperty p = new CodeMemberProperty();
            p.Attributes &= ~MemberAttributes.AccessMask;
            p.Attributes |= MemberAttributes.Public;
            p.Name = elementPropertyName;
            p.Type = new CodeTypeReference(elementTypeName);

            p.CustomAttributes.Add(new CodeAttributeDeclaration(
                new CodeTypeReference(typeof(ConfigurationProperty)),
                new CodeAttributeArgument[] {
                    new CodeAttributeArgument(new CodePrimitiveExpression(elementTagName))
                }));

            p.GetStatements.Add(new CodeMethodReturnStatement(
                new CodeCastExpression(elementTypeName, new CodeIndexerExpression(
                new CodeBaseReferenceExpression(), new CodePrimitiveExpression(elementTagName)))));

            containingType.Members.Add(p);
        }

        void AddConfigAttribute(ScdlAttribute attr, CodeTypeDeclaration type) {
            CodeMemberProperty attrProperty;
            AddConfigAttribute(attr, type, out attrProperty);
        }

        void AddConfigAttribute(ScdlAttribute attr, CodeTypeDeclaration type, out CodeMemberProperty attrProperty) {
            bool isRequired = attr.IsRequired;
            bool isKey = attr.IsKey;
            string propName = NameFromXmlName(attr.Name);
            CodeExpression attribName = new CodePrimitiveExpression(attr.Name);
            Type attrType = attr.ValueType;
            CodeExpression attrDefault;

            if (attrType == typeof(string)) {
                attrDefault = new CodePrimitiveExpression(attr.StringValue);
            }
            else if (attrType == typeof(bool)) {
                attrDefault = new CodePrimitiveExpression(attr.BoolValue);
            }
            else if (attrType == typeof(TimeSpan)) {
                attrDefault = new CodePrimitiveExpression(attr.TimeSpanValue.ToString());
            }
            else if (attrType == typeof(int)) {
                attrDefault = new CodePrimitiveExpression(attr.IntValue);
            }
            else {
                throw new InvalidOperationException(
                    string.Format("Unexpected attribute type '{0}'", attrType.Name));
            }

            // Generate property
            CodeMemberProperty prop = new CodeMemberProperty();
            prop.Attributes &= ~MemberAttributes.AccessMask;
            prop.Attributes |= MemberAttributes.Public;
            prop.Name = propName;
            prop.Type = new CodeTypeReference(attrType);

            prop.CustomAttributes.Add(new CodeAttributeDeclaration(
                new CodeTypeReference(typeof(ConfigurationProperty)),
                new CodeAttributeArgument[] {
                    new CodeAttributeArgument(attribName),
                    new CodeAttributeArgument("DefaultValue", attrDefault),
                    new CodeAttributeArgument("IsKey", new CodePrimitiveExpression(isKey)),
                    new CodeAttributeArgument("IsRequired", new CodePrimitiveExpression(isRequired))
                }));

            prop.GetStatements.Add(new CodeMethodReturnStatement(
                new CodeCastExpression(attrType,
                new CodeIndexerExpression(new CodeBaseReferenceExpression(), attribName))));

            prop.SetStatements.Add(new CodeAssignStatement(
                new CodeIndexerExpression(new CodeBaseReferenceExpression(), attribName),
                new CodePropertySetValueReferenceExpression()));

            type.Members.Add(prop);

            attrProperty = prop;
        }

        static string NameFromXmlName(string xmlName) {
            // uppercase the first letter
            if (string.IsNullOrEmpty(xmlName)) {
                return xmlName;
            }

            if (xmlName.Length == 1) {
                return xmlName.ToUpperInvariant();
            }

            return xmlName.Substring(0, 1).ToUpperInvariant() + xmlName.Substring(1);
        }
    }
}
